package top.z.template.parse.ast.expression;

import top.z.template.Context;
import top.z.template.Error;
import top.z.template.Template;
import top.z.template.interpreter.Reflection;
import top.z.template.parse.Ast.Expression;
import top.z.template.parse.Span;

import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.List;

/**
 * 顶级函数的调用, 函数可以是存储在 {@link Context} 中的 {@link FunctionalInterface}
 */
public class FunctionCall extends Expression {

    private final Expression            function;
    private final List<Expression>      argument;
    private       Object                cacheFunction;
    private final ThreadLocal<Object[]> cacheArgument;

    public FunctionCall(Span span, Expression function, List<Expression> argument) {
        super(span);
        this.function = function;
        this.argument = argument;
        this.cacheArgument = new ThreadLocal<>();
    }

    public Expression getFunction() {
        return function;
    }

    public List<Expression> getArgument() {
        return argument;
    }

    public Object getCacheFunction() {
        return cacheFunction;
    }

    public void setCacheFunction(Object cacheFunction) {
        this.cacheFunction = cacheFunction;
    }

    public Object[] getCacheArgument() {
        Object[] argument = cacheArgument.get();
        if (argument == null) {
            argument = new Object[this.argument.size()];
            cacheArgument.set(argument);
        }
        return argument;
    }

    /**
     * 必须在此节点完成计算时调用, 以免泄漏内存
     */
    public void clearCacheArgument() {
        Arrays.fill(getCacheArgument(), null);
    }

    @Override
    public Object evaluate(Template template, Context context, OutputStream out) throws IOException {
        try {
            Object[]         argumentValue = getCacheArgument();
            List<Expression> arguments     = getArgument();
            for (int index = 0, n = argumentValue.length; index < n; index++) {
                Expression expression = arguments.get(index);
                argumentValue[index] = expression.evaluate(template, context, out);
            }

            // This is a special case to handle template level macros.
            // If a call to a macro is made, evaluating the function expression will result in an exception, as the function name can't be found in the context.
            // Instead we need to manually check if the function expression is a VariableAccess and if so, if it can be found in the context.
            Object function;
            if (getFunction() instanceof Variable) {
                Variable variable = (Variable) getFunction();
                function = context.get(variable.getVariableName().getText());
            } else {
                function = getFunction().evaluate(template, context, out);
            }

            if (function != null) {
                Object method = getCacheFunction();
                if (method != null) {
                    try {
                        return Reflection.getInstance().callMethod(function, method, argumentValue);
                    } catch (Throwable t) {
                        // fall through
                    }
                }
                method = Reflection.getInstance().getMethod(function, null, argumentValue);
                if (method == null) {
                    throw Error.error("Couldn't find function.", getSpan());
                }
                setCacheFunction(method);
                try {
                    return Reflection.getInstance().callMethod(function, method, argumentValue);
                } catch (Throwable t) {
                    throw Error.error(t.getMessage(), getSpan(), t);
                }
            } else {
                // Check if this is a call to a macro defined in this template
                /*if (getFunction() instanceof Variable) {
                    String             functionName = ((VariableAccess) getFunction()).getVariableName().getText();
                    Map<String, Macro> macros       = template.getMacro();
                    Macro              macro        = macros.get(functionName);
                    if (macro != null) {
                        if (macro.getArgumentName().size() != arguments.size())
                            Error.error("Expected " + macro.getArgumentName().size() + " argument, got " + arguments.size(), getSpan());
                        Context macroContext = macro.getContext();

                        // Set all included macros on the macro's context
                        for (String variable : context.getVariable()) {
                            Object value = context.get(variable);
                            if (value instanceof Macro) macroContext.set(variable, value);
                        }

                        // Set the argument, shadowing any included macro names
                        for (int i = 0; i < arguments.size(); i++) {
                            Object arg  = argumentValue[i];
                            String name = macro.getArgumentName().get(i).getText();
                            macroContext.set(name, arg);
                        }

                        Object retVal = AstInterpreter.interpretNodeList(macro.getBody(), macro.getTemplate(), macroContext, out);
                        if (retVal == Return.RETURN_SENTINEL) {
                            return ((Return.ReturnValue) retVal).getValue();
                        } else {
                            return null;
                        }
                    }
                }*/
                throw Error.error("Couldn't find function.", getSpan());
            }
        } finally {
            clearCacheArgument();
        }
    }
}